# -*- coding: utf-8 -*- 
# # # # # # # # # # # # # # # # # # # # #
#                                       #                                       
#    Trabajo Práctico 3 - Conexiones    # 
#                                       #
#     Teoría de las Comunicaciones      #
#      Departamento de Computación      #
#              FCEN - UBA               #
#            junio de 2013              #
#                                       #
# # # # # # # # # # # # # # # # # # # # #


import threading
import random

from common import PacketBuilder, ProtocolControlBlock
from soquete import Soquete
from packet import ACKFlag, FINFlag, SYNFlag
from worker import ClientProtocolWorker
from buffers import DataBuffer, RetransmissionQueue, NotEnoughDataException
from constants import MIN_PACKET_SIZE, MAX_PACKET_SIZE, CLOSED,\
                      ESTABLISHED, FIN_SENT, SYN_SENT, MAX_SEQ,\
                      SEND_WINDOW, MAX_RETRANSMISSION_ATTEMPTS
import time

class ClientControlBlock(ProtocolControlBlock):
    
    def __init__(self, address, port):
        ProtocolControlBlock.__init__(self, address, port)
        # Próximo SEQ a enviar
        self.send_seq = random.randint(1, MAX_SEQ)
        # Tamaño de la ventana de emisión
        self.send_window = SEND_WINDOW
        # Límite inferior de la ventana (i.e., unacknowledged)
        self.window_lo = self.send_seq
        # Límite superior de la ventana
        self.window_hi = self.modular_sum(self.window_lo, self.send_window)
        
    def get_send_seq(self):
        return self.send_seq
    
    def get_send_window(self):
        return self.send_window
    
    def increment_send_seq(self):
        self.send_seq = self.modular_increment(self.send_seq)
    
    def accepts(self, seq_number):
        return self.window_lo <= seq_number and seq_number <= self.send_seq

    def send_allowed(self):
        return self.window_hi != self.send_seq

    def readjust_window(self,ack_number):
        self.window_lo = ack_number
        self.window_hi = self.modular_sum(self.window_lo, self.send_window)

        

class PTCClientProtocol(object):
    
    def __init__(self, address, port):
        self.retransmission_queue = RetransmissionQueue(self)
        self.retransmission_attempts = dict()
        self.outgoing_buffer = DataBuffer()
        self.state = CLOSED
        self.control_block = ClientControlBlock(address, port)
        self.socket = Soquete(address, port)
        self.packet_builder = PacketBuilder(self)
        self.transmition_start_time = 0
        self.total_retransmitions = 0
        self.correct_packets = 0
    
    def is_connected(self):
        return self.state == ESTABLISHED
        
    def build_packet(self, payload=None, flags=None):
        seq = self.control_block.get_send_seq()
        self.control_block.increment_send_seq()
        packet = self.packet_builder.build(payload=payload, flags=flags, seq=seq)
        return packet
        
    def send_packet(self, packet):
        self.socket.send(packet)
        
    def send_and_queue_packet(self, packet):
        self.send_packet(packet)
        self.retransmission_queue.put(packet)
        
    def send(self, data):
        if not self.is_connected():
            raise Exception('cannot send data: connection not established')
        self.worker.send(data)

    def send_block(self,data,times = 1):
        time_avg = 0
        retrans_avg = 0
        correct_packets_avg = 0

        for i in xrange(times):
            self.transmition_start_time = time.time()
            self.total_retransmitions = 0
            self.correct_packets = 0
            self.send(data)
            while not (self.outgoing_buffer.empty() and self.retransmission_queue.empty()):
                time.sleep(0.1)
                
            time_avg += time.time() - self.transmition_start_time
            time.sleep(2)
            retrans_avg += self.total_retransmitions
            correct_packets_avg += self.correct_packets

        print str(time_avg/float(times) ), str(retrans_avg/float(times))


    def connect_to(self, address, port):
        self.worker = ClientProtocolWorker.spawn_for(self)
        self.worker.start()
        self.connected_event = threading.Event()
        self.control_block.set_destination_address(address)
        self.control_block.set_destination_port(port)
        
        syn_packet = self.build_packet(flags=[SYNFlag])
        self.send_and_queue_packet(syn_packet)
        self.state = SYN_SENT
        
        self.connected_event.wait()
    
    def handle_timeout(self):
        packets = [packet[0] for packet in self.retransmission_queue.packet_list]
        self.retransmission_queue.clear()
        for packet in packets:
            seq_number = packet.get_seq_number()
            self.retransmission_attempts[seq_number] = self.retransmission_attempts.get(seq_number,0) + 1
            self.total_retransmitions += 1 #Solo para printearlo en las pruebas
            if self.retransmission_attempts[seq_number] > MAX_RETRANSMISSION_ATTEMPTS:
                self.error = "TIMEOUT! Error al retransmitir paquete", str(seq_number)
                print self.error
                self.shutdown()
            self.send_and_queue_packet(packet)
    
    def handle_pending_data(self):
        more_data_pending = False
        if self.control_block.send_allowed():
            try:
                data = self.outgoing_buffer.get(MIN_PACKET_SIZE, MAX_PACKET_SIZE)
            except NotEnoughDataException:
                pass
            else:
                packet = self.build_packet(payload=data)
                self.send_and_queue_packet(packet)
                
            if not self.outgoing_buffer.empty():
                more_data_pending = True
        else:
            more_data_pending = True
        
        if more_data_pending:
            self.worker.signal_pending_data()
        


    def handle_incoming(self, packet):
        if ACKFlag in packet and self.control_block.accepts(packet.get_ack_number()):
            if self.state == SYN_SENT:
                self.state = ESTABLISHED
                self.connected_event.set()
            elif self.state == FIN_SENT:
                self.state = CLOSED
                self.shutdown()
            self.retransmission_queue.acknowledge(packet)
            self.control_block.readjust_window(packet.get_ack_number())

            self.correct_packets += 1

    def handle_close_connection(self):
        if not self.outgoing_buffer.empty():
            self.worker.signal_pending_data()
            self.worker.signal_close_connection()
        elif not self.retransmission_queue.empty():
            self.worker.signal_close_connection()
        else:
            fin_packet = self.build_packet(flags=[FINFlag])
            self.send_and_queue_packet(fin_packet)
            self.state = FIN_SENT

        
    def close(self):
        if self.is_connected():
            self.worker.signal_close_connection()
        
    def shutdown(self):
        self.outgoing_buffer.clear()
        self.retransmission_queue.clear()
        self.retransmission_attempts.clear()
        self.worker.stop()
        # Esto es por si falló el establecimiento de conexión (para destrabar al thread principal)
        self.connected_event.set()
        self.state = CLOSED

    def change_window_size(self, size):
        self.control_block.send_window = size
